Skip to content

feat(skeleton): define the last-call record once at donkey.last_call (#362) - #376

Merged
tommasobolis merged 1 commit into
developfrom
feat/362-last-call-record
Sep 15, 2026
Merged

tommasobolis merged 1 commit into
developfrom
feat/362-last-call-record

Conversation

@tommasobolis

Copy link
Copy Markdown
Collaborator

What

Closes #362. Defines the last-call record once, in one place, with one name, and surfaces gateway identity on the success path.

On a refusal, DonkeyError already hands the developer the gateway's ids; on a 200 the same information was thrown on the floor. This adds one named container — LastCall — populated on every governed model call from the LIVE-VERIFIED §3 response headers, reachable at donkey.last_call.

The one load-bearing decision (please sanity-check)

Accessor location: dedicated donkey.last_call returning a contextvar-scoped LastCall, not donkey.budget.last_usage.

Rationale (issue hazard #2): Budget gets away with instance-scoped last-write-wins because a token window genuinely is one shared resource. "The last call" is not — under the parallel fan-out donkey.run() encourages, an instance attribute would return whichever response landed last, which looks authoritative and is worse than nothing. So the record lives in a ContextVar, matching how core.telemetry scopes the correlation id. #307 (usage tokens) and #309 (routing/fallback) add their field groups to this same type rather than inventing a parent.

Changes

  • core/lastcall.py — LastCall + LastCallStatus (OBSERVED / UNOBSERVED / UNAVAILABLE, hazard #3). Parses x-request-id and x-envoy-decorator-operation (api-instance-<id>.<env>.svc → api_instance_id + environment_id). Absent/unparseable → None, never raises (§0.3).
  • core/transport.py — both async and blocking _on_response observe the record, gated on _request_model (the same signal that opens a GenAI span), so token fetches / registry GETs sharing the transport never clobber it.
  • donkey.py — donkey.last_call passes through an OBSERVED record; else derives UNAVAILABLE (naming the surface) when every resolved adapter is non-observing; else UNOBSERVED.
  • integrations/ — Adapter.observes_last_call flag; False on adk/crewai (LiteLLM owns the transport) and llamaindex/agent_framework (default_headers only).
  • simulator/fixtures.py — x-envoy-decorator-operation kept in the replay allow-list, so simulate()/donkey mock populate the record from the committed fixtures.
  • tests/conformance/ — gateway_identity_observed scenario + asserted exemptions mirroring §8.1 correlation_id_propagated; a test pins the exemption set to the observes_last_call=False adapters so the table and the code cannot drift.
  • docs/verified-apis.md §3 — notes what x-envoy-decorator-operation is now parsed into.

Explicitly out of scope (per the issue)

Routing/fallback → #309; cached/reasoning tokens → #307; Budget monotonicity + fan-out test → #311; whether the gateway echoes the client correlation id (gateway_correlation_id deliberately not added yet) → #300.

Acceptance criteria

All 11 met — see the issue. Notably: one named record with a single accessor path; identity fields on a 200; None-not-raise for absent/unparseable headers; async and blocking transports populate identically; contextvar concurrency scope with a fan-out test; UNAVAILABLE on LiteLLM-backed adapters instead of a bare None; the exemption asserted (not skipped); no UnverifiedValueWarning.

Gates

pytest -q → 321 passed, 37 skipped, 2 deselected · mypy --strict clean · ruff check . clean · lint-imports 4/4 kept.

🤖 Generated with Claude Code

…362)

Close the identity asymmetry: on a refusal, DonkeyError already hands the
gateway's ids; on a 200 the same information was thrown on the floor. Add one
named container, LastCall, populated on every governed model call from the
LIVE-VERIFIED §3 response headers and reachable at donkey.last_call.

- core/lastcall.py: the LastCall record + LastCallStatus (OBSERVED /
  UNOBSERVED / UNAVAILABLE), parses x-request-id and x-envoy-decorator-operation
  (api-instance-<id>.<env>.svc → api_instance_id + environment_id). Unparseable
  or absent headers leave fields None and never raise (§0.3). Contextvar-scoped,
  not instance-scoped (hazard #2), matching core.telemetry's correlation id.
  #307/#309 add their field groups to this same type, no new parent.
- transport.py: both async and blocking _on_response observe the record on a
  model call only (gated on _request_model, the same signal that opens a span),
  so token fetches / registry GETs sharing the transport do not clobber it.
- donkey.py: donkey.last_call passes through an OBSERVED record; else derives
  UNAVAILABLE (naming the surface) when every resolved adapter is non-observing,
  else UNOBSERVED — the three honest states, never a bare None (hazard #3).
- integrations: Adapter.observes_last_call flag; False on adk/crewai (LiteLLM
  owns the transport) and llamaindex/agent_framework (default_headers only).
- simulator/fixtures.py: keep x-envoy-decorator-operation in the replay
  allow-list, so simulate()/donkey mock populate the record from the fixtures.
- tests/conformance: gateway_identity_observed scenario + asserted exemptions
  mirroring §8.1 correlation_id_propagated; a test pins the exemption set to the
  observes_last_call=False adapters so table and code cannot drift.
- docs/verified-apis.md §3: note what x-envoy-decorator-operation is parsed into.

The response x-correlation-id is deliberately NOT parsed into the record yet —
whether it echoes the client-sent id is unconfirmed (#300, hazard #1).

Closes #362

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tommasobolis
tommasobolis merged commit f367c37 into develop Sep 15, 2026
7 checks passed
@tommasobolis
tommasobolis deleted the feat/362-last-call-record branch September 15, 2026 06:40
tommasobolis added a commit that referenced this pull request Sep 15, 2026
#382)

Release: promote develop → main (donkey.last_call skeleton, #376–#378)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant